package org.mm.proxycache.http.codec;

import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

import org.apache.mina.common.IoBuffer;
import org.mm.proxycache.http.HttpChunk;
import org.mm.proxycache.http.HttpDecoderException;
import org.mm.proxycache.http.HttpMessage;
import org.mm.proxycache.http.HttpMethod;
import org.mm.proxycache.http.HttpResponseStatus;
import org.mm.proxycache.http.HttpUrl;
import org.mm.proxycache.http.HttpVersion;
import org.mm.proxycache.http.MutableHttpMessage;
import org.mm.proxycache.http.MutableHttpRequest;
import org.mm.proxycache.http.MutableHttpResponse;

public class HttpParser
{
    public enum State {
        MethodStart, Method, UriStart, Uri, HttpVersionH, HttpVersionT1, HttpVersionT2,
        HttpVersionP, HttpVersionSlash, HttpVersionMajor, HttpVersionDot, HttpVersionMinor,
        ExpectingCR, ExpectingLF, HeaderLineStart, HeaderLineContinue, HeaderName,
        SpaceBeforeHeaderValue, HeaderValue, FinalNewline, ReadBody, ChunkLength, ChunkLengthStart,
        ChunkLengthTail, ChunkBody, StatusCodeStart, StatusCode, StatusCodeText, ChunkBodyCR,
        ChunkBodyLF, ReadBodyAdLibitum
    };
    
    public enum ParserStatus {
        Done, NeedData, Error
    };
    
    public enum ParserType {
        RequestParser, ResponseParser
    };
    
    // ////////////////////////////////////////////////
    
    private State state;
    
    private MutableHttpMessage message;
    
    private ParserType parserType;
    
    private int readBytes = 0;
    
    private int chunkLength = 0;
    
    private Queue<Object> parsedList = new ConcurrentLinkedQueue<Object>();
    
    private StringBuilder sb = new StringBuilder( 40 );
    private StringBuilder value = new StringBuilder( 100 );
    
    private int n = 0;
    
    // ////////////////////////////////////////////////
    
    public HttpParser( ParserType type )
    {
        parserType = type;
        reset();
    }
    
    public State state()
    {
        return state;
    }
    
    public void reset()
    {
        parsedList.clear();
        sb.setLength( 0 );
        value.setLength( 0 );
        readBytes = 0;
        chunkLength = 0;
        
        if ( parserType == ParserType.RequestParser )
        {
            message = new DefaultHttpRequest();
            state = State.MethodStart;
        }
        else
        {
            message = new DefaultHttpResponse();
            state = State.HttpVersionH;
        }
    }
    
    public HttpMessage getMessage()
    {
        return message;
    }
    
    public Object getParsed()
    {
        return parsedList.poll();
    }
    
    public ParserStatus decode( IoBuffer in ) throws HttpDecoderException
    {       
        while ( in.hasRemaining() )
        {
            char c = (char) in.get();
           
            switch ( state )
            {
            case MethodStart:
                if ( !isChar( c ) || isCtl( c ) || isTSpecial( c ) )
                    throw new HttpDecoderException( this, in );
                
                state = State.Method;
                sb.append( c );
                break;
            
            case Method:
                if ( c == ' ' )
                {
                    state = State.Uri;
                    HttpMethod method = HttpMethod.parse( sb.toString() );
                    sb.setLength( 0 );
                    
                    if ( method == null )
                        throw new HttpDecoderException( this, in );
                    
                    ((MutableHttpRequest) message).method( method );
                }
                else if ( !isChar( c ) || isCtl( c ) || isTSpecial( c ) )
                    throw new HttpDecoderException( this, in );
                else
                    sb.append( c );
                break;
            
            case UriStart:
                if ( isCtl( c ) )
                    throw new HttpDecoderException( this, in );
                
                state = State.Uri;
                sb.append( c );
                break;
            
            case Uri:
                if ( c == ' ' )
                {
                    state = State.HttpVersionH;
                    HttpUrl url = HttpUrl.parse( sb.toString() );
                    sb.setLength( 0 );
                    
                    if ( url == null )
                        throw new HttpDecoderException( this, in );
                    
                    ((MutableHttpRequest) message).requestUrl( url );
                    break;
                }
                else if ( isCtl( c ) )
                    throw new HttpDecoderException( this, in );
                else
                    sb.append( c );
                
                break;
            
            case HttpVersionH:
                if ( c != 'H' )
                    throw new HttpDecoderException( this, in );
                
                state = State.HttpVersionT1;
                break;
            
            case HttpVersionT1:
                if ( c != 'T' )
                    throw new HttpDecoderException( this, in );
                
                state = State.HttpVersionT2;
                break;
            
            case HttpVersionT2:
                if ( c != 'T' )
                    throw new HttpDecoderException( this, in );
                
                state = State.HttpVersionP;
                break;
            
            case HttpVersionP:
                if ( c != 'P' )
                    throw new HttpDecoderException( this, in );
                
                state = State.HttpVersionSlash;
                break;
            
            case HttpVersionSlash:
                if ( c != '/' )
                    throw new HttpDecoderException( this, in );
                
                state = State.HttpVersionMajor;
                break;
            
            case HttpVersionMajor:
                if ( c != '1' )
                    throw new HttpDecoderException( this, in );
                
                state = State.HttpVersionDot;
                break;
            
            case HttpVersionDot:
                if ( c != '.' )
                    throw new HttpDecoderException( this, in );
                
                state = State.HttpVersionMinor;
                break;
            
            case HttpVersionMinor:
                if ( c == '0' )
                    message.version( HttpVersion.Http10 );
                else if ( c == '1' )
                    message.version( HttpVersion.Http11 );
                else
                    throw new HttpDecoderException( this, in );
                
                if ( parserType == ParserType.RequestParser )
                    state = State.ExpectingCR;
                else
                    state = State.StatusCodeStart;
                break;
            
            case StatusCodeStart:
                if ( c != ' ' )
                    throw new HttpDecoderException( this, in );
                
                n = 0;
                state = State.StatusCode;
                break;
            
            case StatusCode:
                if ( c == ' ' )
                {
                    ((MutableHttpResponse) message).status( HttpResponseStatus.forId( n ) );
                    state = State.StatusCodeText;
                }
                else if ( !isDigit( c ) )
                    throw new HttpDecoderException( this, in );
                else
                    n = n * 10 + (c - '0');
                
                break;
            
            case StatusCodeText:
                if ( c == '\r' )
                {
                    state = State.ExpectingLF;
                    ((MutableHttpResponse) message).statusReasonPhrase( sb.toString() );
                    sb.setLength( 0 );
                }
                else if ( c == '\n' )
                {
                    // Handles malformed messages
                    state = State.HeaderLineStart;
                    ((MutableHttpResponse) message).statusReasonPhrase( sb.toString() );
                    sb.setLength( 0 );
                }
                else
                    sb.append( c );
                break;
            
            case ExpectingCR:
                if ( c != '\r' )
                    throw new HttpDecoderException( this, in );
                
                state = State.ExpectingLF;
                break;
            
            case ExpectingLF:
                if ( c != '\n' )
                    throw new HttpDecoderException( this, in );
                
                state = State.HeaderLineStart;
                break;
            
            case HeaderLineStart:
                if ( c == '\n' )
                {
                    c = '\r';
                    in.position( in.position() - 1 );
                }
                
                if ( c == '\r' )
                    state = State.FinalNewline;
                
                // TODO: Continued line headers
                // else if ( ! msg->m_headers.empty() && (c == ' '|| c == '\t')
                // )
                // {
                // m_state = header_lws;
                // break;
                // }
                else if ( !isChar( c ) || isCtl( c ) || isTSpecial( c ) )
                    throw new HttpDecoderException( this, in );
                else
                {
                    sb.append( c );
                    state = State.HeaderName;
                }
                break;
            
            case HeaderName:
                if ( c == ':' )
                    state = State.SpaceBeforeHeaderValue;
                else if ( (!isChar( c ) || isCtl( c ) || isTSpecial( c )) && c != ' ' )
                    throw new HttpDecoderException( this, in );
                else
                    sb.append( c );
                
                break;
            
            case SpaceBeforeHeaderValue:
                if ( c != ' ' )
                    in.position( in.position() - 1 );
                
                state = State.HeaderValue;
                break;
            
            case HeaderValue:
                if ( c == '\n' )
                {
                    // Handles a malformed HTTP message
                    c = '\r';
                    in.position( in.position() - 1 );
                }
                
                if ( c == '\r' )
                {
                    state = State.ExpectingLF;
                    message.header( sb.toString().trim(), value.toString().trim() );
                    sb.setLength( 0 );
                    value.setLength( 0 );
                }
                else if ( isCtl( c ) )
                    throw new HttpDecoderException( this, in );
                else
                    value.append( c );
                
                break;
            
            case FinalNewline:
                if ( c != '\n' )
                    throw new HttpDecoderException( this, in );
                
                parsedList.add( message );
                
                if ( message.contentLength() > 0 )
                {
                    state = State.ReadBody;
                    break;
                }
                else if ( message.isChunked() )
                {
                    state = State.ChunkLengthStart;
                    break;
                }
                else if ( parserType == ParserType.ResponseParser
                        && (message.version() == HttpVersion.Http10 || !message.isKeepAlive()) )
                {
                    // Some servers do not specify content-length nor chunked,
                    // so we have to wait for the connection to be closed to be
                    // sure that the content is terminated.
                    state = State.ReadBodyAdLibitum;
                    break;
                }
                else if ( in.hasRemaining() )
                {
                    message.contentLength( in.remaining() );
                    state = State.ReadBody;
                    break;
                }
                
                parsedList.add( HttpChunk.lastChunk( message ) );
                return ParserStatus.Done;
                
            case ReadBody:
                in.position( in.position() - 1 );
                int remaining = (int) message.contentLength() - readBytes;
                int dataLen = Math.min( in.remaining(), remaining );
                
                if ( remaining == 0 )
                    return ParserStatus.Done;
                else if ( dataLen == remaining )
                {
                    parsedList.add( new HttpChunk( message, in, dataLen ) );
                    parsedList.add( HttpChunk.lastChunk( message ) );
                    return ParserStatus.Done;
                }
                else
                {
                    readBytes += in.remaining();
                    parsedList.add( new HttpChunk( message, in, dataLen ) );
                }
                break;
            
            case ReadBodyAdLibitum:
                in.position( in.position() - 1 );
                if ( in.remaining() > 0 )
                    parsedList.add( new HttpChunk( message, in, in.remaining() ) );
                break;
            
            case ChunkLengthStart:
                if ( !isHexDigit( c ) )
                    throw new HttpDecoderException( this, in );
                
                sb.append( c );
                state = State.ChunkLength;
                break;
            
            case ChunkLength:
                if ( isHexDigit( c ) )
                    sb.append( c );
                else
                {
                    state = State.ChunkLengthTail;
                    chunkLength = Integer.parseInt( sb.toString(), 16 );
                    sb.setLength( 0 );
                    readBytes = 0;
                    
                    if ( chunkLength == 0 )
                    {
                        parsedList.add( HttpChunk.lastChunk( message ) );
                        return ParserStatus.Done;
                    }
                }
                break;
            
            case ChunkLengthTail:
                if ( c == '\n' )
                    state = State.ChunkBody;
                
                break;
            
            case ChunkBody:
                in.position( in.position() - 1 );
                
                int chunkRemaining = chunkLength - readBytes;
                int chunkDataLen = Math.min( chunkRemaining, in.remaining() );
                
                parsedList.add( new HttpChunk( message, in, chunkDataLen ) );
                readBytes += chunkDataLen;
                
                if ( chunkDataLen == chunkRemaining )
                    state = State.ChunkBodyCR;
                break;
            
            case ChunkBodyCR:
                if ( c != '\r' )
                    throw new HttpDecoderException( this, in );
                
                state = State.ChunkBodyLF;
                break;
            
            case ChunkBodyLF:
                if ( c != '\n' )
                    throw new HttpDecoderException( this, in );
                
                state = State.ChunkLength;
                break;
            
            default:
                break;
            }
        }
        
        return ParserStatus.NeedData;
    }
    
    private static boolean isHexDigit( char c )
    {
        return HEX_DEC[c] != -1;
    }
    
    private static boolean isDigit( char c )
    {
        return (c >= '0' && c <= '9');
    }
    
    private static boolean isChar( char c )
    {
        return c >= 0 && c <= 127;
    }
    
    private static boolean isCtl( char c )
    {
        return HTTP_CONTROLS[c];
    }
    
    private static boolean isTSpecial( char c )
    {
        return HTTP_SEPARATORS[c];
    }
    
    /**
     * A lookup table from ASCII char values to corresponding decimal values
     */
    private static final int[] HEX_DEC = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 00, 01, 02, 03, 04, 05, 06, 07, 8, 9,
            -1, -1, -1, -1, -1, -1, -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1,
            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 10, 11, 12, 13, 14,
            15, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, };
    
    /**
     * A lookup table for HTPP separator characters
     */
    private static boolean[] HTTP_SEPARATORS = new boolean[128];
    
    /**
     * A lookup table for HTTP control characters
     */
    private static boolean[] HTTP_CONTROLS = new boolean[128];
    
    static
    {
        // HTTP Separator characters
        HTTP_SEPARATORS[34] = true; // "
        HTTP_SEPARATORS[40] = true; // )
        HTTP_SEPARATORS[41] = true; // (
        HTTP_SEPARATORS[44] = true; // ,
        HTTP_SEPARATORS[47] = true; // /
        HTTP_SEPARATORS[58] = true; // :
        HTTP_SEPARATORS[59] = true; // ;
        HTTP_SEPARATORS[60] = true; // <
        HTTP_SEPARATORS[61] = true; // =
        HTTP_SEPARATORS[62] = true; // >
        HTTP_SEPARATORS[63] = true; // ?
        HTTP_SEPARATORS[64] = true; // @
        HTTP_SEPARATORS[91] = true; // [
        HTTP_SEPARATORS[93] = true; // ]
        HTTP_SEPARATORS[92] = true; // \
        HTTP_SEPARATORS[123] = true; // {
        HTTP_SEPARATORS[125] = true; // }
        
        // HTTP Control characters
        for ( int i = 0; i <= 31; ++i )
        {
            HTTP_CONTROLS[i] = true;
        }
        HTTP_CONTROLS[127] = true; // DEL
        
    }
}
